const nodeTypeEnum = {
    BEGIN: 0,
    END: 1,
    ARROW: 2,

    OPEN_PAGE: 3,
    CLICK_ELEMENT: 4,
    LOOP: 5,
    EXTRACT_DATA: 6,
    CONDITION: 7,
    BRANCH: 8,
}

const dataTypeEnum = {
    TEXT: 0,
    URL: 1,
    TEXT_URL: 2,
    NONE: 10
}

const extraDataType = {
    PAGE_URL: 50,
    PAGE_TITLE: 51,
    EXTRACT_TIME: 52
}

const loopTypeEnum = {
    LOOP_URL: 1,
    LOOP_PAGING: 3,
    LOOP_LIST: 2
}

const reprocessEnum = {
    REG_EXP: 100,
    CLEAR_EMPTY: 101,
    ADD_PREFIX: 102,
    ADD_SUFFIX: 103,
}

const conditionTypeEnum = {
    TEXT: 0,
    ELEMENT: 1,
}

class Node {
    #id
    #name
    #type
    #html
    #beforeExec = 0
    #beforeExecIsRandom = false

    toJSON() {
        return {
            id: this.#id,
            name: this.#name,
            type: this.#type,
            beforeExec: this.#beforeExec,
            beforeExecIsRandom: this.#beforeExecIsRandom,
        };
    }

    // 该节点在页面上对应的元素

    constructor(name, type, id) {
        this.#id = id
        this.#name = name
        this.#type = type
    }

    setBeforeExec(beforeExec) {
        this.#beforeExec = beforeExec
    }

    getBeforeExec() {
        return this.#beforeExec
    }

    setBeforeExecIsRandom(beforeExecIsRandom) {
        this.#beforeExecIsRandom = beforeExecIsRandom
    }

    getBeforeExecIsRandom() {
        return this.#beforeExecIsRandom
    }

    setHtml(html) {
        this.#html = html
    }

    getHtml() {
        if (!this.#html) {
            this.#html = this.generateNodeHTML()

        }
        return this.#html
    }

    getType() {
        return this.#type
    }

    getName() {
        return this.#name
    }

    setName(name) {
        this.#name = name
    }

    setId(id) {
        this.#id = id
    }

    getId() {
        return this.#id
    }

    // 生成节点的HTML，不同类型的节点会有所不同
    generateNodeHTML() {
        const type = this.#type
        const id = this.#id
        const name = this.#name
        switch (type) {
            case nodeTypeEnum.BEGIN:
                return `<img draggable="false" src="../../resources/start-task.svg" class="begin start-end" id="${id}"><img draggable="false" src="../../resources/arrow-down.svg" class="arrow" id="${generateNextId(id)}">`
            case nodeTypeEnum.END:
                return `<img draggable="false" src="../../resources/end-task.svg" class="end start-end" id="${id}" style="margin-bottom: 50px">`
            case nodeTypeEnum.ARROW:
                return `<img draggable="false" src="../../resources/arrow-down.svg" class="arrow" id="${id}">`
            case nodeTypeEnum.OPEN_PAGE:
                return `<div draggable="true" class="open-page t1" id="${id}">${name}</div><img draggable="false" src="../../resources/arrow-down.svg" class="arrow" id="${generateNextId(id)}">`
            case nodeTypeEnum.CLICK_ELEMENT:
                return `<div draggable="true" class="click-element t1" id="${id}">${name}</div><img draggable="false" src="../../resources/arrow-down.svg" class="arrow" id="${generateNextId(id)}">`
            case nodeTypeEnum.EXTRACT_DATA:
                return `<div draggable="true" class="extract-data t1" id="${id}">${name}</div><img draggable="false" src="../../resources/arrow-down.svg" class="arrow" id="${generateNextId(id)}">`
            case nodeTypeEnum.LOOP:
                return `<div class="loop t2" id="${id}"><div draggable="true" class="t2-1">${name}</div><img draggable="false" src="../../resources/arrow-down.svg" class="arrow" id="${id}-0"></div><img draggable="false" src="../../resources/arrow-down.svg" class="arrow" id="${generateNextId(id)}">`
            case nodeTypeEnum.BRANCH:
                return `<div class="branch t2" id="${id}"><div class="t2-1">${name}</div><img draggable="false" src="../../resources/arrow-down.svg" class="arrow" id="${id}-0"></div><span style="color: #cccccc;margin: 7px 0">or</span>`
            case nodeTypeEnum.CONDITION:
                return `<div class="condition t2" id="${id}"><div draggable="true" class="t2-1">${name}</div><img draggable="false" src="../../resources/add-branch.svg" class="add-branch" title="添加分支"></div><img draggable="false" src="../../resources/arrow-down.svg" class="arrow" id="${generateNextId(id)}">`
        }
    }
}

class BeginNode extends Node {
    constructor(id) {
        super('', nodeTypeEnum.BEGIN, id)
    }
}

class EndNode extends Node {
    constructor(id) {
        super('', nodeTypeEnum.END, id)
    }
}

class ArrowNode extends Node {
    constructor(id) {
        super('', nodeTypeEnum.ARROW, id)
    }
}

// 节点-打开网页
class OpenPageNode extends Node {
    // 如果在循环中，该字段为null
    #url
    #timeout = 0
    #retryTimes = 0
    #retryInterval = 0

    toJSON() {
        return {
            ...super.toJSON(),
            url: this.#url,
            retryTimes: this.#retryTimes,
            retryInterval: this.#retryInterval,
            timeout: this.#timeout,
        }
    }

    constructor(id, name = "打开网页1", url = null) {
        super(name, nodeTypeEnum.OPEN_PAGE, id)
        this.#url = url
    }

    setUrl(url) {
        this.#url = url
    }

    getUrl() {
        return this.#url
    }

    setRetryInterval(retryInterval) {
        this.#retryInterval = retryInterval
    }

    getRetryInterval() {
        return this.#retryInterval
    }

    setRetryTimes(retryTimes) {
        this.#retryTimes = retryTimes
    }

    getRetryTimes() {
        return this.#retryTimes
    }

    setTimeout(timeout) {
        this.#timeout = timeout
    }

    getTimeout() {
        return this.#timeout
    }

}

// 节点-点击界面元素
class ClickElementNode extends Node {
    // 如果在循环中，该字段为null
    #xpath
    // 用来标记该点击行为是否是在翻页
    #isPaging

    toJSON() {
        return {
            ...super.toJSON(),
            xpath: this.#xpath,
            isPaging: this.#isPaging
        }
    }

    constructor(id, name = "点击元素1", xpath = null, isPaging = false) {
        super(name, nodeTypeEnum.CLICK_ELEMENT, id)
        this.#xpath = xpath
        this.#isPaging = isPaging
    }

    getXpath() {
        return this.#xpath
    }

    setXpath(xpath) {
        this.#xpath = xpath
    }

    getIsPaging() {
        return this.#isPaging
    }

    setIsPaging(isPaging) {
        this.#isPaging = isPaging
    }
}


// 节点-提取数据
class ExtractDataNode extends Node {
    // 如果在循环中，该字段为null
    #xpath
    #dataType
    // 指示要提取的数据[{name:xxx,type:xxx,reprocess:[xxx,xxx]},...]。其中的每一项与数据预览表格的列一一对应
    #data

    toJSON() {
        return {
            ...super.toJSON(),
            xpath: this.#xpath,
            dataType: this.#dataType,
            data: this.#data
        }
    }

    constructor(dataType, id, name = "提取数据1", xpath = null) {
        super(name, nodeTypeEnum.EXTRACT_DATA, id)
        this.#dataType = dataType
        this.#xpath = xpath
        switch (dataType) {
            case dataTypeEnum.TEXT_URL:
                this.#data = [{name: "字段1", type: dataTypeEnum.TEXT, reprocess: []}, {
                    name: "字段2",
                    type: dataTypeEnum.URL,
                    reprocess: []
                }]
                break
            case dataTypeEnum.TEXT:
            case dataTypeEnum.URL:
                this.#data = [{name: "字段1", type: dataType, reprocess: []}]
                break
            case dataTypeEnum.NONE:
                this.#data = []
                break
        }
    }

    getXpath() {
        return this.#xpath
    }

    setXpath(xpath) {
        this.#xpath = xpath
    }

    getDataType() {
        return this.#dataType
    }

    setDataType(dataType) {
        this.#dataType = dataType
    }


    delData(idx) {
        this.#data.splice(idx, 1)
    }

    addData(item) {
        this.#data.push(item)
    }

    getData() {
        return this.#data
    }

    setData(d) {
        this.#data = d;
    }
}


// 节点-循环
class LoopNode extends Node {
    #loopType
    #subNodes
    // 要循环的项对应的xpath
    #xpathList
    // xpath对应的元素项个数
    // #xpathMatchNum
    // 如果是循环打开网页的场景，使用该字段
    #urls
    // 默认为0，表示循环次数不限
    #loopTimes

    toJSON() {
        return {
            ...super.toJSON(),
            loopType: this.#loopType,
            subNodes: this.#subNodes,
            xpathList: this.#xpathList,
            urls: this.#urls,
            loopTimes: this.#loopTimes,
            // xpathMatchNum: this.#xpathMatchNum,
        };
    }

    constructor(loopType, id, xpathList, name = "循环1", loopTimes = 0, urls = null) {
        console.log(xpathList)
        super(name, nodeTypeEnum.LOOP, id)
        this.#loopType = loopType
        this.#loopTimes = loopTimes
        this.#subNodes = [new ArrowNode(id + '-0')]
        this.#xpathList = objectCopy(xpathList)
        this.#urls = urls
    }


    addNode(node) {
        this.#subNodes.push(node)
    }

    deleteNode(node) {
        this.#subNodes.splice(this.#subNodes.indexOf(node), 1)
    }

    setSubNodes(subNodes) {
        this.#subNodes = subNodes
    }

    getSubNodes() {
        return this.#subNodes
    }

    getXpathList() {
        return this.#xpathList
    }

    setXpathList(xpathList) {
        this.#xpathList = xpathList
    }

    getUrls() {
        return this.#urls
    }

    setUrls(urls) {
        this.#urls = urls
    }

    getLoopType() {
        return this.#loopType
    }

    setLoopType(loopType) {
        this.#loopType = loopType
    }

    getLoopTimes() {
        return this.#loopTimes
    }

    setLoopTimes(times) {
        this.#loopTimes = times
    }
}

// 节点-判断条件
class ConditionNode extends Node {
    #subNodes

    toJSON() {
        return {
            ...super.toJSON(),
            subNodes: this.#subNodes
        }
    }

    constructor(id, name = "条件判断1") {
        super(name, nodeTypeEnum.CONDITION, id)
        this.#subNodes = []
    }

    addNode(node) {
        this.#subNodes.push(node)
    }

    deleteNode(node) {
        this.#subNodes.splice(this.#subNodes.indexOf(node), 1)
    }

    getSubNodes() {
        return this.#subNodes
    }
}

// 节点-分支，是条件节点的子节点
class BranchNode extends Node {
    // 判断类型：基于元素/基于文本
    #conditionType
    // 用与进行条件判断的值，可能是正则表达式，也可能是xpath
    #pattern
    #subNodes

    toJSON() {
        const tmp = {
            ...super.toJSON(),
            conditionType: this.#conditionType,
            pattern: this.#pattern,
            subNodes: this.#subNodes
        };
        console.log(tmp)
        return tmp
    }

    constructor(id, name = "分支1", conditionType, pattern) {
        super(name, nodeTypeEnum.BRANCH, id)
        this.#subNodes = [new ArrowNode(id + '-0')]
        this.#conditionType = conditionType
        this.#pattern = pattern
    }

    addNode(node) {
        this.#subNodes.push(node)
    }

    deleteNode(node) {
        this.#subNodes.splice(this.#subNodes.indexOf(node), 1)
    }

    getSubNodes() {
        return this.#subNodes
    }

    setPattern(pattern) {
        this.#pattern = pattern
    }

    getPattern() {
        return this.#pattern
    }

    setConditionType(conditionType) {
        this.#conditionType = conditionType
    }

    getConditionType() {
        return this.#conditionType
    }
}


// 完整的任务流程图
class TaskFlowGenerator {
    // 一个多层列表，对应流程图中节点。节点在该列表中的位置与节点在页面上的位置要建立双向关系，知道一个位置即可知道对应的另一个位置
    // 采用数组索引与元素id对应的方案。例如 id='2-3-4'的html节点就对应nodes[2][3][4]
    #nodes
    // 采集数据的字段列表
    #dataFieldList
    // 下一个要插入节点的位置。如果未指定，则默认插入上一次插入的节点的后面
    #nextNodeAddPos
    // 各类节点统计
    #nodesCount = new Map([
        [nodeTypeEnum.LOOP, 0],
        [nodeTypeEnum.EXTRACT_DATA, 0],
        [nodeTypeEnum.OPEN_PAGE, 0],
        [nodeTypeEnum.CLICK_ELEMENT, 0],
        [nodeTypeEnum.BRANCH, 0],
        [nodeTypeEnum.CONDITION, 0]
    ])

    constructor() {
        const beginNode = new BeginNode('0')
        const endNode = new EndNode('2')
        this.#nodes = [beginNode, new ArrowNode('1'), endNode]
        $("#task-flow").append(beginNode.getHtml() + endNode.getHtml());
        // 下一个插入元素的位置
        this.#nextNodeAddPos = [2]
        this.#dataFieldList = []

        const taskFlow = $('#task-flow')
        // 鼠标悬浮在流程图箭头上，变为加号表示可以插入节点
        taskFlow.on('mouseover', '.arrow', (event) => {
            const element = event.target
            element.src = '../../resources/node-add.svg';
            // element.dataset.hasMouseoverListener = 'true'
        })
        // 移走箭头恢复
        taskFlow.on('mouseout', '.arrow', (event) => {
            const element = event.target
            element.src = '../../resources/arrow-down.svg';
            // element.dataset.hasMouseoverListener = 'true'
        })
        // 点击流程图中箭头上的加号，显示自定义菜单
        taskFlow.on('click', '.arrow', (event) => {
            const $element = $(event.target);
            showCustomMenu(event, 'add-node-menu')
            // 记录插入点
            insertNodePos = $element.attr('id');
        })

        // 点击条件判断中的加号，添加新分支
        taskFlow.on('click', '.add-branch', (event) => {
            const parent = $(event.target.parentElement)
            const branchNum = parent.children('.branch').length
            const newBranch = buildEmptyNode(nodeTypeEnum.BRANCH, parent.attr('id') + `-${branchNum}`)
            newBranch.setName(`分支${branchNum + 1}`)
            taskFlowGenerator.addNode(newBranch)
        })

        // 点击流程图节点，变色表示选中
        taskFlow.on('click', '.t2-1,.t1', (event) => {
            this.#selectNode(event.target)
        })
        taskFlow.on('mouseover', '.t2-1,.t1', (event) => {
            if (event.target === selectedNode) {
                return
            }
            const e = $(event.target)
            e.css('background-color', 'rgba(30,159,255,0.14)')
            if (e.hasClass('t2-1')) {
                e.parent().css('border-color', 'rgba(30,159,255,0.43)')
            } else {
                e.css('border-color', 'rgba(30,159,255,0.43)')
            }
        })
        taskFlow.on('mouseout', '.t2-1,.t1', (event) => {
            const e = $(event.target)
            if (event.target === selectedNode) {
                return
            }
            e.css('background-color', '#f3f3f3')
            if (e.hasClass('t2-1')) {
                e.parent().css('border-color', '#e6e6e6')
            } else {
                e.css('border-color', '#e6e6e6')
            }
        })
    }

    addNode(node) {
        // 插入分支节点单独处理
        if (node.getType() === nodeTypeEnum.BRANCH) {
            const parentId = node.getId().split('-').slice(0, -1).join('-')
            // dom插入元素
            $(`#${parentId}`).append(node.getHtml())
            // taskflow 插入节点
            const parentNode = taskFlowGenerator.getNode(parentId)
            parentNode.getSubNodes().push(node)

            // 调整下一次插入的位置为该 branch 内
            const newPos = node.getId().split('-')
            newPos.push(1)
            this.#nextNodeAddPos = newPos.map(Number)
            return
        }

        let arr = this.#nodes
        // 能进入该循环说明插入的元素不在最外层
        for (let i = 0; i < this.#nextNodeAddPos.length - 1; i++) {
            arr = arr[this.#nextNodeAddPos[i]].getSubNodes()
        }
        const currentLayerIdx = this.#nextNodeAddPos[this.#nextNodeAddPos.length - 1]
        // 插入节点前先修改当前层后续节点的id(后移)，此时的arr表示的是要插入节点的列表
        for (let i = currentLayerIdx; i < arr.length; i++) {
            console.log(arr[i])
            this.fixId(arr[i], this.#nextNodeAddPos.slice(0, this.#nextNodeAddPos.length - 1), i, this.#nextNodeAddPos.length - 1, 2)
        }
        arr.splice(this.#nextNodeAddPos[this.#nextNodeAddPos.length - 1], 0, node, new ArrowNode(generateNextId(node.getId())))
        // 获取要插入节点的上一个兄弟节点，基于这个节点的位置注入html。我们可以保证当前要插入位置之前一定有元素
        let tmp = objectCopy(this.#nextNodeAddPos)
        tmp[tmp.length - 1]--
        console.log(tmp.join('-'))
        $(`#${tmp.join('-')}`).after(node.getHtml());

        // 修改下一个要插入节点的位置
        switch (node.getType()) {
            case nodeTypeEnum.LOOP:
            case nodeTypeEnum.BRANCH:
            case nodeTypeEnum.CONDITION:
                this.#nextNodeAddPos.push(1)
                break
            default:
                this.#nextNodeAddPos[this.#nextNodeAddPos.length - 1] += 2
                break
        }
        // 更新节点统计
        const type = node.getType()
        this.#nodesCount.set(type, this.#nodesCount.get(type) + 1)
        console.log('add', this)
    }

    // 插入或删除元素时修正某一元素及其子元素的id值,pos表示要更改id中的第几个数字
    fixId(node, prefixIdx, currentLayerIdx, pos, adder) {
        switch (node.getType()) {
            case nodeTypeEnum.LOOP:
            case nodeTypeEnum.BRANCH:
            case nodeTypeEnum.CONDITION:
                // 有子节点需要递归
                const children = node.getSubNodes()
                const tmp = objectCopy(prefixIdx)
                tmp.push(currentLayerIdx)
                for (let i = 0; i < children.length; i++) {
                    this.fixId(children[i], tmp, i, pos, adder)
                }
        }
        // 修正id对应位置上的数字（+1）
        if (prefixIdx.length === 0) {
            $(`#${currentLayerIdx}`).attr('id', currentLayerIdx + adder + '')
            node.setId(currentLayerIdx + adder + '')
        } else {
            const oldPrefix = prefixIdx.join('-') + '-' + currentLayerIdx
            const newPrefix = oldPrefix.split('-')
            newPrefix[pos] = (parseInt(newPrefix[pos]) + adder).toString()
            const newId = newPrefix.join('-')
            $(`#${oldPrefix}`).attr('id', newId)
            node.setId(newId)
        }
    }

    #selectNode(element) {
        if (selectedNode != null) {
            this.cancelSelectNode(selectedNode)
        }
        element.style.backgroundColor = '#1e9fff'
        element.style.color = '#ffffff'
        let node
        if (element.className.includes('t1')) {
            element.style.border = '4px solid #1e9fff'
            node = this.getNode(element.id)
        } else {
            element.parentElement.style.border = '4px solid #1e9fff'
            node = this.getNode(element.parentElement.id)
        }
        selectedNode = element
        // 显示节点配置面板
        $('.optional').hide()
        $(`.p${node.getType()}`).show()
        showNodeConfig()
        // 渲染数据
        this.#renderNodeData(node)
        if (node.getType() === nodeTypeEnum.EXTRACT_DATA && node.getDataType() !== dataTypeEnum.NONE) {
            let condition = null
            let parentNode = null
            if (node.getId().includes('-')) {
                parentNode = this.getNode(element.parentElement.id)
                // 该提取数据节点位于一个条件分支中
                if (parentNode.getType() === nodeTypeEnum.BRANCH) {
                    condition = {
                        type: parentNode.getConditionType(),
                        pattern: parentNode.getPattern()
                    }
                }
            }

            // 处于循环中的数据提取节点
            if (node.getXpath() === null) {
                if (condition == null) {
                    wv.send('previewData', parentNode.getXpathList(), node.getData(), condition)
                } else {
                    wv.send('previewData', this.getNode(element.parentElement.parentElement.parentElement.id).getXpathList(), node.getData(), condition)
                }
            } else {
                wv.send('previewData', [node.getXpath()], node.getData(), condition)
            }
            dataPreviewNode = element
        }

        // 修正下一次要插入节点的位置为此次选中的元素之后。
        // Branch节点是个例外，插入位置会修改为该Branch中最后的位置，因为我们不能允许用户在Condition节点内插入非branch节点
        const pos = node.getId().split('-').map(Number)
        if (node.getType() === nodeTypeEnum.BRANCH) {
            pos.push(node.getSubNodes().length)
        } else {
            pos[pos.length - 1] += 2
        }
        this.#nextNodeAddPos = pos
    }

    #renderNodeData(node) {
        // 通用属性渲染
        $('#ipt-step-name').val(node.getName())
        $('#before-wait').val(node.getBeforeExec())
        $('#random-wait').prop('checked', node.getBeforeExecIsRandom())
        let ele
        // 其他属性渲染
        switch (node.getType()) {
            case nodeTypeEnum.OPEN_PAGE:
                $('#timeout').val(node.getTimeout())
                $('#retry-times').val(node.getRetryTimes())
                $('#retry-interval').val(node.getRetryInterval())
                ele = $('#ipt-page-url')
                // 不是循环打开网页
                if (node.getUrl() != null) {
                    ele.val(node.getUrl())
                    ele.removeAttr("readonly");
                    $('#use-loop-url').prop('checked', false)
                } else {
                    ele.val('使用循环中的URL')
                    ele.prop('readonly', true)
                    $('#use-loop-url').prop('checked', true)
                }
                break
            case nodeTypeEnum.EXTRACT_DATA:
                // 设置提取的数据类型
                const dataTypeListElement = $('#selector-dataType')
                const tmp1 = $('.p6 .layui-select-title input')
                switch (node.getDataType()) {
                    case dataTypeEnum.TEXT:
                        dataTypeListElement.val(dataTypeEnum.TEXT + '')
                        tmp1.val('文本')
                        break
                    case dataTypeEnum.URL:
                        dataTypeListElement.val(dataTypeEnum.URL + '')
                        tmp1.val('链接')
                        break
                    case dataTypeEnum.TEXT_URL:
                        dataTypeListElement.val(dataTypeEnum.TEXT_URL + '')
                        tmp1.val('文本+链接')
                        break
                    case dataTypeEnum.NONE:
                        dataTypeListElement.val(dataTypeEnum.NONE + '')
                        tmp1.val('无')
                        break
                }
            // 继续执行下一个case
            case nodeTypeEnum.CLICK_ELEMENT:
                ele = $('#ipt-xpath')
                if (node.getXpath() != null) {
                    ele.val(node.getXpath())
                    ele.removeAttr("readonly");
                    $('#use-loop-xpath').prop('checked', false)
                } else {
                    ele.val('使用循环中的元素')
                    ele.prop('readonly', true)
                    $('#use-loop-xpath').prop('checked', true)
                }
                break
            case nodeTypeEnum.LOOP:
                if (node.getLoopTimes() !== 0) {
                    $('#ipt-loop-times').val(node.getLoopTimes())
                    $('#ck-use-loop-times').prop('checked', true)
                    $('#loop-times').css("visibility", "visible")
                } else {
                    $('#ipt-loop-times').val(0)
                    $('#ck-use-loop-times').prop('checked', false)
                    $('#loop-times').css("visibility", "visible")
                }

                const loopTypeListElement = $('#selector-loopType')
                const tmp = $('.p5 .layui-select-title input')
                switch (node.getLoopType()) {
                    case loopTypeEnum.LOOP_URL:
                        $('#ta-url').val(node.getUrls().join('\n'))
                        // 设置该类型循环对应的循环类型
                        $('.loop-url').css('display', 'block')
                        loopTypeListElement.val(loopTypeEnum.LOOP_URL + '')
                        tmp.val('循环网页')
                        break
                    case loopTypeEnum.LOOP_PAGING:
                        $('#ipt-xpath').val(node.getXpathList()[0])
                        $('.loop-paging').css('display', 'block')
                        loopTypeListElement.val(loopTypeEnum.LOOP_PAGING + '')
                        tmp.val('循环单元素-点击')
                        break
                    case loopTypeEnum.LOOP_LIST:
                        $('#ta-xpath').val(node.getXpathList().join('\n'))
                        $('.loop-list').css('display', 'block')
                        console.log(1991)
                        loopTypeListElement.val(loopTypeEnum.LOOP_LIST + '')
                        tmp.val('循环元素列表')
                        break
                }
                break
            case nodeTypeEnum.BRANCH:
                $('#condition-pattern').val(node.getPattern())
                $('#selector-conditionType').val(node.getConditionType() + '')
                if (node.getConditionType() === conditionTypeEnum.TEXT) {
                    $('.p8 .layui-select-title input').val('基于文本(正则表达式匹配)')
                } else {
                    $('.p8 .layui-select-title input').val('基于元素(xpath定位)')
                }
                break
        }
    }

    cancelSelectNode(element) {
        element.style.backgroundColor = '#f3f3f3'
        element.style.color = '#000000'
        if (element.className.includes('t1')) {
            element.style.border = '4px solid #e6e6e6'
        } else {
            element.parentElement.style.border = '4px solid #e6e6e6'
        }
        selectedNode = null
    }

    getNextNodeAddPos() {
        return this.#nextNodeAddPos
    }

    setNextNodeAddPos(nextNodeAddPos) {
        this.#nextNodeAddPos = nextNodeAddPos
    }

    getNodeCount(type) {
        return this.#nodesCount.get(type)
    }

    getNode(id) {
        id = id.split('-')
        let arr = this.#nodes
        for (let i = 0; i < id.length - 1; i++) {
            arr = arr[id[i]].getSubNodes()
        }
        return arr[id[id.length - 1]]
    }

    #getNodeParent(node) {
        const idArr = node.getId().split('-')
        // 没有父元素
        if (idArr.length === 1) {
            return null
        }
        return this.getNode(idArr.slice(0, idArr.length - 1).join('-'))
    }

    deleteNode(id) {
        id = id.split('-').map(Number)
        let arr = this.#nodes
        for (let i = 0; i < id.length - 1; i++) {
            arr = arr[id[i]].getSubNodes()
        }

        // 删除的节点如果是分支节点，需要特殊处理
        if (arr[0].getType() === nodeTypeEnum.BRANCH) {
            console.log('delete branch')
            const currentLayerIdx = id[id.length - 1] + 1
            for (let i = currentLayerIdx; i < arr.length; i++) {
                this.fixId(arr[i], id.slice(0, id.length - 1), i, id.length - 1, -1)
            }
            arr.splice(id[id.length - 1], 1)
            this.#nextNodeAddPos = id.slice(0, -1)
            this.#nextNodeAddPos[id.length - 1] += 2
        } else {
            console.log('delete other')
            const currentLayerIdx = id[id.length - 1] + 2
            // 修正当前层后续节点的id，此时的arr表示的是要删除节点的列表
            for (let i = currentLayerIdx; i < arr.length; i++) {
                this.fixId(arr[i], id.slice(0, id.length - 1), i, id.length - 1, -2)
            }
            // 删除目标节点以及后面的一个箭头节点
            arr.splice(id[id.length - 1], 2)
            // 修正下一个要插入节点的位置为此次删除的节点的位置
            this.#nextNodeAddPos = id
        }

    }

    toJSON() {
        return {
            dataFieldList: this.#dataFieldList,
            nodes: this.#nodes
        }
    }

    // 获取任务的json描述
    getTask() {
        // 遍历所有ExtractData节点，填充dataFieldList字段
        this.buildDataFieldList(this.#nodes)

        const noDataNodes = [0, 1, 2]
        return JSON.stringify(this,
            (k, v) => {
                // 过滤掉非数据节点（起始节点和箭头节点）
                if (k === 'nodes' || k === 'subNodes') {
                    return v.filter((item) => {
                        return !noDataNodes.includes(item.getType())
                    })
                }
                return v
            }, 2)
    }

    // 遍历所有数据提取节点，收集数据字段
    buildDataFieldList(nodes) {
        for (let node of nodes) {
            switch (node.getType()) {
                case nodeTypeEnum.EXTRACT_DATA:
                    for (let col of node.getData()) {
                        if (!this.#dataFieldList.includes(col.name)) {
                            this.#dataFieldList.push(col.name)
                        }
                    }
                    break
                case nodeTypeEnum.LOOP:
                case nodeTypeEnum.CONDITION:
                case nodeTypeEnum.BRANCH:
                    this.buildDataFieldList(node.getSubNodes())
                    break
            }
        }
    }
}

// --------------------------------- 一些工具函数 ------------------------------------------
function generateNextId(id) {
    const arr = id.split('-')
    arr[arr.length - 1] = (parseInt(arr[arr.length - 1]) + 1).toString()
    return arr.join('-')
}

function buildEmptyNode(type, id) {
    switch (type) {
        case nodeTypeEnum.OPEN_PAGE:
            return new OpenPageNode(id, `打开网页${taskFlowGenerator.getNodeCount(nodeTypeEnum.OPEN_PAGE) + 1}`, '')
        case nodeTypeEnum.CLICK_ELEMENT:
            return new ClickElementNode(id, `点击元素${taskFlowGenerator.getNodeCount(nodeTypeEnum.CLICK_ELEMENT) + 1}`, '')
        case nodeTypeEnum.EXTRACT_DATA:
            return new ExtractDataNode(dataTypeEnum.NONE, id, `提取数据${taskFlowGenerator.getNodeCount(nodeTypeEnum.EXTRACT_DATA) + 1}`, '')
        case nodeTypeEnum.LOOP:
            return new LoopNode(loopTypeEnum.LOOP_LIST, id, [], `循环${taskFlowGenerator.getNodeCount(nodeTypeEnum.LOOP) + 1}`)
        case nodeTypeEnum.BRANCH:
            return new BranchNode(id, `分支${taskFlowGenerator.getNodeCount(nodeTypeEnum.BRANCH) + 1}`, conditionTypeEnum.TEXT, "")
        case nodeTypeEnum.CONDITION:
            return new ConditionNode(id, `判断条件${taskFlowGenerator.getNodeCount(nodeTypeEnum.CONDITION) + 1}`)
    }
}


function extractData(contentType) {
    let id
    // 判断有几个元素数据要提取，如果不止一个，则要额外插入循环节点
    if (selectedNum > 1) {
        taskFlowGenerator.addNode(new LoopNode(loopTypeEnum.LOOP_LIST, taskFlowGenerator.getNextNodeAddPos().join('-'), selectedXpathList,
            `循环列表${taskFlowGenerator.getNodeCount(nodeTypeEnum.LOOP) + 1}`))
        id = taskFlowGenerator.getNextNodeAddPos().join('-')
        taskFlowGenerator.addNode(new ExtractDataNode(contentType, id,
            `提取数据${taskFlowGenerator.getNodeCount(nodeTypeEnum.EXTRACT_DATA) + 1}`))
    } else {
        id = taskFlowGenerator.getNextNodeAddPos().join('-')
        taskFlowGenerator.addNode(new ExtractDataNode(contentType, id,
            `提取数据${taskFlowGenerator.getNodeCount(nodeTypeEnum.EXTRACT_DATA) + 1}`, selectedXpathList[0]))

    }
    // 记录数据预览对应的数据提取节点
    dataPreviewNode = document.getElementById(id)
    // 插入提取数据节点的同时进行数据预览。新插入节点时不会有扩展数据，故列表为空
    wv.send('previewData', selectedXpathList, taskFlowGenerator.getNode(id).getData())
    // 让内容渲染进程清除选中状态，以便下一轮的选择
    cancelSelect()
}

function showCustomMenu(event, id) {
    // 获取自定义菜单元素
    const $customMenu = $(`#${id}`);
    // 显示自定义菜单
    $customMenu.css({
        display: 'block',
        left: `${event.clientX}px`,
        top: `${event.clientY}px`
    });

    // 限制菜单显示范围（避免超出视口）
    const menuWidth = $customMenu.outerWidth();
    const menuHeight = $customMenu.outerHeight();
    const viewportWidth = $(window).width();
    const viewportHeight = $(window).height();

    if (event.clientX + menuWidth > viewportWidth) {
        $customMenu.css('left', `${viewportWidth - menuWidth}px`);
    }
    if (event.clientY + menuHeight > viewportHeight) {
        $customMenu.css('top', `${viewportHeight - menuHeight}px`);
    }
}

function cancelSelect() {
    wv.send('cancelSelect')
    $('#select-status').text(`请通过鼠标选择网页内容`)
    allowPreSelectStatus = true
    selectedNum = 0
    selectedXpathList = []
    preSelectNum = 0
}

function objectCopy(obj) {
    return JSON.parse(JSON.stringify(obj))
}

function renderTable(cols, data) {
    const tb = layui.table.render({
        elem: '#ID-table-demo-data',
        cols: [cols],
        data: data,
        height: '#table-0',
    })
    // 显示其他字段操作的图标
    $('#data-display-tb th div').each((idx, ele) => {
        if ($(ele).parent().attr('data-field') === '-1') {
            return
        }
        $(ele).append('<img src="../../resources/col-operation.svg" id="col-operation">')
    })
    return tb
}

function reloadTable(cols, data) {
    dataPreviewTable.reload('ID-table-demo-data', {
        cols: [cols],
        data: data,
        height: '#table-0',
    })
    $('#data-display-tb th div').each((idx, ele) => {
        if ($(ele).parent().attr('data-field') === '-1') {
            return
        }
        $(ele).append('<img src="../../resources/col-operation.svg" id="col-operation">')
    })
}

function addExtraData(type, d) {
    // 重新进行数据预览渲染
    // 添加新列
    const cols = dataPreviewTable.config.cols
    const colNum = cols[0].length
    cols[0].push({field: `${colNum - 1}`, title: `字段${colNum}`})
    // 更新节点的数据提取模型
    const node = taskFlowGenerator.getNode(dataPreviewNode.id)
    node.addData({name: `字段${colNum}`, type: type, reprocess: []})
    // 每行添加新数据项
    const data = dataPreviewTable.config.data
    for (let i = 0; i < data.length; i++) {
        data[i][`${colNum - 1}`] = d
    }
    // 重新渲染表格
    reloadTable(cols, data)
    originData = objectCopy(data)
}

// 删除某一列的数据
function deleteDataCol() {
    const node = taskFlowGenerator.getNode(dataPreviewNode.id)
    node.delData(selectedCol)

    const cols = dataPreviewTable.config.cols
    // 删除表头列
    cols[0].splice(selectedCol + 1, 1)
    const colNum = cols[0].length
    // 修改后续列的field，保证field能准确反映列的位置
    for (let i = selectedCol + 1; i < colNum; i++) {
        cols[0][i].field = i - 1 + ''
    }

    const data = dataPreviewTable.config.data
    // 删除每行数据对应的字段，并处理field变动带来的影响
    for (let i = 0; i < data.length; i++) {
        for (let j = selectedCol + 1; j < colNum; j++) {
            data[i][j - 1 + ''] = data[i][j + '']
        }
        delete data[i][colNum - 1 + '']
    }
    console.log(cols, data, colNum)
    // 重新渲染表格
    reloadTable(cols, data)
}

// 显示数据后处理配置面板
function showDataReprocessBoard() {
    const node = taskFlowGenerator.getNode(dataPreviewNode.id)
    // .reprocess  {type:xxx,param:xxx}  从ExtractDataNode的data字段提取出该数据列的后处理配置
    const reprocess = node.getData()[selectedCol].reprocess
    $("#list").html('')
    for (let item of reprocess) {
        let text
        switch (parseInt(item.type)) {
            case reprocessEnum.REG_EXP:
                text = "正则匹配"
                break
            case reprocessEnum.CLEAR_EMPTY:
                text = "去除空字符"
                break
            case reprocessEnum.ADD_PREFIX:
                text = "添加前缀"
                break
            case reprocessEnum.ADD_SUFFIX:
                text = "添加后缀"
                break
        }
        // 向列表中添加已配置的后处理步骤
        const newItem = $(`<li id="${item.type}"></li>`)
            .addClass("list-item")
            .html(`${text}<span class="delete-icon">×</span>`)
            .attr("data-action", $(this).attr("data-action"));
        $("#list").append(newItem);
    }

    reprocessStepList = objectCopy(reprocess)

    // 显示面板
    $('#data-display-tb').hide()
    $('#reprocess-operate-background').show()
}

// 根据任务的json定义，复原出任务流程图
function buildTaskFlow(nodeList) {
    for (let node of nodeList) {
        let newNode
        switch (node.type) {
            case nodeTypeEnum.OPEN_PAGE:
                newNode = new OpenPageNode(node.id, node.name, node.url)
                taskFlowGenerator.addNode(newNode)
                newNode.setTimeout(node.timeout)
                newNode.setRetryTimes(node.retryTimes)
                newNode.setRetryInterval(node.retryInterval)
                break
            case nodeTypeEnum.CLICK_ELEMENT:
                newNode = new ClickElementNode(node.id, node.name, node.xpath, node.isPaging)
                taskFlowGenerator.addNode(newNode)
                break
            case nodeTypeEnum.EXTRACT_DATA:
                newNode = new ExtractDataNode(node.dataType, node.id, node.name, node.xpath)
                newNode.setData(node.data)
                taskFlowGenerator.addNode(newNode)
                break
            case nodeTypeEnum.LOOP:
            case nodeTypeEnum.CONDITION:
            case nodeTypeEnum.BRANCH:
                if (node.type === nodeTypeEnum.LOOP) {
                    newNode = new LoopNode(node.loopType, node.id, node.xpathList, node.name, node.loopTimes, node.urls)
                } else if (node.type === nodeTypeEnum.CONDITION) {
                    newNode = new ConditionNode(node.id, node.name)
                } else {
                    newNode = new BranchNode(node.id, node.name, node.conditionType, node.pattern)
                }
                const nextPos = objectCopy(taskFlowGenerator.getNextNodeAddPos())
                taskFlowGenerator.addNode(newNode)
                buildTaskFlow(node.subNodes)
                if (node.type === nodeTypeEnum.BRANCH) {
                    nextPos[nextPos.length - 1] += 1
                } else {
                    nextPos[nextPos.length - 1] += 2
                }
                taskFlowGenerator.setNextNodeAddPos(nextPos)
                break
        }
        // 设置公共属性
        newNode.setBeforeExec(node.beforeExec)
        newNode.setBeforeExecIsRandom(node.beforeExecIsRandom)
    }
}

// -------------------------------------------------------------------------

console.log("taskflow_generate.js loaded!")